package com.yhb.service.impl;

import com.yhb.channel.ChannelConstant;
import com.yhb.channel.ChannelDispatcher;
import com.yhb.channel.FlowService;
import com.yhb.channel.dao.flow.ExecutionFlow;
import com.yhb.channel.exception.*;
import com.yhb.channel.provider.INotifyProvider;
import com.yhb.channel.utils.ChannelStringUtils;
import com.yhb.conf.ResultCode;
import com.yhb.controller.out.ThirdApiController;
import com.yhb.controller.vo.SimpleVerifyCodeTokenVo;
import com.yhb.dao.RapidCheatMapper;
import com.yhb.dao.ThirdPurchaseFlowMapper;
import com.yhb.model.*;
import com.yhb.service.*;
import com.yhb.utils.Constants;
import com.yhb.utils.DateUtils;
import com.yhb.utils.StringUtils;
import com.yhb.yinsheng.UnionPayService;
import lombok.extern.log4j.Log4j;
import lombok.var;
import me.edzh.spring.bootstrap.processor.SignAndEncryptProcessor;
import me.edzh.spring.bootstrap.service.SMSService;
import me.edzh.spring.bootstrap.utils.JSONUtil;
import me.edzh.spring.bootstrap.utils.exception.GlobalResponseException;
import me.edzh.spring.bootstrap.utils.exception.ServiceException;
import me.edzh.spring.bootstrap.utils.http.ClientHolder;
import okhttp3.*;
import org.apache.commons.codec.binary.Hex;
import org.apache.http.util.TextUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service("tradeService")
@Log4j
public class TradeServiceImpl implements TradeService, INotifyProvider {

    @Autowired
    private ChannelFlowService channelFlowService;

    @Autowired
    private RapidCheatMapper rapidCheatMapper;

    @Autowired
    private CardService cardService;

    @Autowired
    private SMSService smsService;

    @Autowired
    private UserService userService;

    @Autowired
    private RedisTemplate<String, String> redis;

    @Autowired
    private UnionPayService unionPayService;

    @Autowired
    private MessageService messageService;

    @Autowired
    private ChannelDispatcher channelDispatcher;

    @Autowired
    private FlowService flowService;

    @Autowired
    private ThirdPurchaseFlowMapper thirdPurchaseFlowMapper;

    @Autowired
    private ThirdPartyService thirdPartyService;

    @Autowired
    private CommissionService commissionService;


    private static final Map<String, Mac> sThirdMacs = new HashMap<>();


    @Lazy
    @Qualifier("MQServiceImpl")
    @Autowired
    private MQService mqService;

    private static final String KEY_PREFIX = "rapid-cheat-";
    private static final Long RAPID_CHEAT_VERIFY_CODE_TIMEOUT = 5 * 60 * 1000L; // 5min

    @Override
    public SimpleVerifyCodeTokenVo commitRapidCheat(String consumeCardId, String remitCardId, BigDecimal amount, User user, String province, String city, String district) {
        if (amount.compareTo(new BigDecimal(30)) < 0) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "单笔金额最低为30元");
        }
        log.info("rapid cheat card id:" + consumeCardId + "  " + remitCardId + "  " + amount.toString());
        Card consumeCard = cardService.cardWithOwnerCheck(consumeCardId, user.getId());
        Card remitCard = cardService.cardWithOwnerCheck(remitCardId, user.getId());
        BigDecimal commission = channelFlowService.calculateCommission(user, consumeCard, amount);

        RapidCheat cheat = RapidCheat.builder()
                .type(RapidCheat.Type.COMMON_CHANNEL)
                .consumeCardId(consumeCardId)
                .remitCardId(remitCardId)
                .userId(user.getId())
                .amount(amount)
                .remitFee(channelFlowService.calculateRemitFee(user, amount, ChannelFlowService.RemitType.RAPID))
                .consumeCardBank(consumeCard.getBankName())
                .consumeCardType(consumeCard.getType())
                .consumeCardNumber(consumeCard.getNumber())
                .remitCardBank(remitCard.getBankName())
                .remitCardType(remitCard.getType())
                .remitCardNumber(remitCard.getNumber())
                .commission(commission)
                .province(province)
                .city(city)
                .district(district)
                .createTime(new Date())
                .status(RapidCheat.Status.CREATED)
                .build();

        if (channelDispatcher.getChannelReadyInfo(consumeCard, remitCard, amount)) {
            //判断渠道消费是否短信验证
            if(channelDispatcher.checkPayChannelSmsVerify(consumeCard, remitCard, amount)){
                String consumeId = null;
                try {
                    BigDecimal payRate = BigDecimal.ONE
                            .subtract(BigDecimal.ONE.multiply(channelFlowService.getCommissionRate(user, consumeCard)))
                            .multiply(BigDecimal.valueOf(100))
                            .setScale(2, RoundingMode.CEILING);
                    consumeId = channelDispatcher.pollingTryPay(user.getId(),
                            consumeCard,
                            remitCard,
                            amount,
                            commission,
                            payRate.toString(),
                            Constants.getCurrentRemoteIp(),
                            cheat.getProvince(),
                            cheat.getCity(),
                            cheat.getDistrict(), true);
                } catch (TryingBindCardExceptionWrapper e) {
                    redis.boundValueOps(KEY_PREFIX + e.getToken()).set(JSONUtil.toJson(cheat), 5 * RAPID_CHEAT_VERIFY_CODE_TIMEOUT, TimeUnit.MILLISECONDS);

                    log.info("CODE_HAS_MORE_VERIFY_CODE: " +  e.getToken());
                    throw new GlobalResponseException(
                            ResultCode.OK_CODE,
                            e.getMessage(),
                            SimpleVerifyCodeTokenVo.builder()
                                    .token(e.getToken())
                                    .number(StringUtils.getMaskString(consumeCard.getPhoneNumber(), 3, 4, 4, "*"))
                                    .build());
                } catch (PayChannelException e) {
                    if (e.getCode() == ChannelConstant.PAY_ERROR) {
                        cheat.setStatus(RapidCheat.Status.CONSUME_FAILED);
                        cheat.setConsumeFlowId(e.getExtendInfo()); // 约定: 当 errorCode 是 PAY_ERROR 时, extendInfo 就是 flowId
                        rapidCheatMapper.modify(cheat);
                    }
                    throw e;
                }
            }else{
                return sendVerifyCode(consumeCard, cheat);
            }
        } else {

            BigDecimal payRate = BigDecimal.ONE
                    .subtract(BigDecimal.ONE.multiply(channelFlowService.getCommissionRate(user, consumeCard)))
                    .multiply(BigDecimal.valueOf(100))
                    .setScale(2, RoundingMode.CEILING);

            try {
                String consumeId = channelDispatcher.pollingTryPay(user.getId(),
                        consumeCard,
                        remitCard,
                        amount,
                        commission,
                        payRate.toString(),
                        Constants.getCurrentRemoteIp(),
                        province, city, district, false);

                // 理论上这里走不到, 如果走到了, 正常入库
                cheat.setConsumeFlowId(consumeId);
                rapidCheatMapper.insert(cheat);

            } catch (TryingBindCardExceptionWrapper e) {
                redis.boundValueOps(KEY_PREFIX + e.getToken()).set(JSONUtil.toJson(cheat), 5 * RAPID_CHEAT_VERIFY_CODE_TIMEOUT, TimeUnit.MILLISECONDS);

                throw new GlobalResponseException(
                        ResultCode.OK_CODE,
                        e.getMessage(),
                        SimpleVerifyCodeTokenVo.builder()
                                .token(e.getToken())
                                .number(e.getPhoneNumber())
                                .build());
            } catch (CardAlreadyBoundException e) {
                sendVerifyCode(consumeCard, cheat);
            }
        }

        throw new ServiceException(ResultCode.ERROR_INTERNAL_DATA_CODE, "内部错误, 请{{联系客服}}");

    }

    private SimpleVerifyCodeTokenVo sendVerifyCode(Card consumeCard, RapidCheat cheat) {
        String code = (int) ((Math.random() * 9 + 1) * 100000) + "";
        cheat.setVerifyCode(code);

        HashMap<String, String> param = new HashMap<>();
        param.put("code", code);
        smsService.sendSms(consumeCard.getPhoneNumber(), "SMS_174025563", param);
        String uuid = UUID.randomUUID().toString();
        redis.boundValueOps(KEY_PREFIX + uuid).set(JSONUtil.toJson(cheat), RAPID_CHEAT_VERIFY_CODE_TIMEOUT, TimeUnit.MILLISECONDS);
        log.info("sendVerifyCode: " + code + ", uuid: " + uuid);
        throw new GlobalResponseException(
                ResultCode.OK_CODE,
                consumeCard.getBankName() + ChannelStringUtils.getMaskString(consumeCard.getNumber(), 0, 3, 4) + "验证码",
                SimpleVerifyCodeTokenVo.builder()
                        .token(uuid)
                        .number(StringUtils.getMaskString(consumeCard.getPhoneNumber(), 3, 4, 4, "*"))
                        .build());
    }

    @Override
    public RapidCheat executeRapidCheat(String token, String verifyCode, User user) {
        log.info("executeRapidCheat " + token + "    " + verifyCode);

        String content = redis.boundValueOps(KEY_PREFIX + token).get();

        if (content == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "验证码超时");
        }

        RapidCheat cheatInfo = JSONUtil.parseJson(content, RapidCheat.class);
        Card consumeCard = cardService.cardWithOwnerCheck(cheatInfo.getConsumeCardId(), user.getId());
        Card remitCard = cardService.cardWithOwnerCheck(cheatInfo.getRemitCardId(), user.getId());

        BigDecimal amount = cheatInfo.getAmount();
        BigDecimal commission = channelFlowService.calculateCommission(user, consumeCard, amount);


        // 对流水先入库, 再根据状态修改
        RapidCheat cheat = RapidCheat.builder()
                .consumeCardId(cheatInfo.getConsumeCardId())
                .remitCardId(cheatInfo.getRemitCardId())
                .userId(user.getId())
                .amount(amount)
                .type(RapidCheat.Type.COMMON_CHANNEL)
                .status(RapidCheat.Status.CREATED)
                .commission(commission)
                .remitFee(channelFlowService.calculateRemitFee(user, amount, ChannelFlowService.RemitType.RAPID))
                .consumeCardBank(consumeCard.getBankName())
                .consumeCardType(consumeCard.getType())
                .consumeCardNumber(consumeCard.getNumber())
                .remitCardBank(remitCard.getBankName())
                .remitCardType(remitCard.getType())
                .remitCardNumber(remitCard.getNumber())
                .createTime(new Date())
                .build();

        rapidCheatMapper.insert(cheat);

        String consumeId = null;

        if (cheatInfo.getVerifyCode() != null) {

            if (!user.getId().equals(cheatInfo.getUserId())) {
                rapidCheatMapper.remove(cheat.getId());
                throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "不是我的验证码");
            }
            if(channelDispatcher.checkPayChannelSmsVerify(consumeCard, remitCard, amount)){
                //渠道短信验证
                try {
                    consumeId = channelDispatcher.verifyPay(user.getId(), token, verifyCode, cheatInfo.getProvince(), cheatInfo.getCity(), cheatInfo.getDistrict());
                } catch (TryingBindCardExceptionWrapper e) {
                    redis.boundValueOps(KEY_PREFIX + e.getToken()).set(JSONUtil.toJson(cheat), 5 * RAPID_CHEAT_VERIFY_CODE_TIMEOUT, TimeUnit.MILLISECONDS);

                    throw new GlobalResponseException(
                            ResultCode.CODE_HAS_MORE_VERIFY_CODE,
                            e.getMessage(),
                            SimpleVerifyCodeTokenVo.builder()
                                    .token(e.getToken())
                                    .number(e.getPhoneNumber())
                                    .build());
                } catch (PayChannelException e) {
                    if (e.getCode() == ChannelConstant.PAY_ERROR) {
                        cheat.setStatus(RapidCheat.Status.CONSUME_FAILED);
                        cheat.setConsumeFlowId(e.getExtendInfo()); // 约定: 当 errorCode 是 PAY_ERROR 时, extendInfo 就是 flowId
                        rapidCheatMapper.modify(cheat);
                    }

                    throw e;
                }
            }else {
                if (!cheatInfo.getVerifyCode().equals(verifyCode)) {
                    rapidCheatMapper.remove(cheat.getId());
                    throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "验证码错误");
                }

                redis.delete(KEY_PREFIX + token);

                try {
                    BigDecimal payRate = BigDecimal.ONE
                            .subtract(BigDecimal.ONE.multiply(channelFlowService.getCommissionRate(user, consumeCard)))
                            .multiply(BigDecimal.valueOf(100))
                            .setScale(2, RoundingMode.CEILING);
                    consumeId = channelDispatcher.pollingTryPay(user.getId(),
                            consumeCard,
                            remitCard,
                            amount,
                            commission,
                            payRate.toString(),
                            Constants.getCurrentRemoteIp(),
                            cheatInfo.getProvince(),
                            cheatInfo.getCity(),
                            cheatInfo.getDistrict(), true);

                } catch (TryingBindCardExceptionWrapper e) {
                    redis.boundValueOps(KEY_PREFIX + e.getToken()).set(JSONUtil.toJson(cheat), 5 * RAPID_CHEAT_VERIFY_CODE_TIMEOUT, TimeUnit.MILLISECONDS);

                    log.info("CODE_HAS_MORE_VERIFY_CODE: " + e.getToken());
                    throw new GlobalResponseException(
                            ResultCode.CODE_HAS_MORE_VERIFY_CODE,
                            e.getMessage(),
                            SimpleVerifyCodeTokenVo.builder()
                                    .token(e.getToken())
                                    .number(e.getPhoneNumber())
                                    .build());
                } catch (PayChannelException e) {
                    if (e.getCode() == ChannelConstant.PAY_ERROR) {
                        cheat.setStatus(RapidCheat.Status.CONSUME_FAILED);
                        cheat.setConsumeFlowId(e.getExtendInfo()); // 约定: 当 errorCode 是 PAY_ERROR 时, extendInfo 就是 flowId
                        rapidCheatMapper.modify(cheat);
                    }

                    throw e;
                }
            }
        } else {
            try {
                consumeId = channelDispatcher.verifyPay(user.getId(), token, verifyCode, cheatInfo.getProvince(), cheatInfo.getCity(), cheatInfo.getDistrict());
            } catch (TryingBindCardExceptionWrapper e) {
                redis.boundValueOps(KEY_PREFIX + e.getToken()).set(JSONUtil.toJson(cheat), 5 * RAPID_CHEAT_VERIFY_CODE_TIMEOUT, TimeUnit.MILLISECONDS);

                throw new GlobalResponseException(
                        ResultCode.CODE_HAS_MORE_VERIFY_CODE,
                        e.getMessage(),
                        SimpleVerifyCodeTokenVo.builder()
                                .token(e.getToken())
                                .number(e.getPhoneNumber())
                                .build());
            } catch (PayChannelException e) {
                if (e.getCode() == ChannelConstant.PAY_ERROR) {
                    cheat.setStatus(RapidCheat.Status.CONSUME_FAILED);
                    cheat.setConsumeFlowId(e.getExtendInfo()); // 约定: 当 errorCode 是 PAY_ERROR 时, extendInfo 就是 flowId
                    rapidCheatMapper.modify(cheat);
                }

                throw e;
            }
        }

        cheat.setConsumeFlowId(consumeId);
        rapidCheatMapper.modify(cheat);


        cheat.setProvince(null);
        cheat.setCity(null);
        cheat.setDistrict(null);
        return cheat;
    }

    @Override
    public RapidCheat rapidFlow(String flowId) {
        RapidCheat query = rapidCheatMapper.query(flowId);
        if (query == null) throw new ServiceException(ResultCode.ERROR_PARAMETER_WRONG, "信息错误");
        return query;
    }

    @Override
    public RapidCheat rapidFlow(String flowId, String userId) {
        RapidCheat cheat = rapidFlow(flowId);
        if (cheat.getUserId().equals(userId)) {
            if (cheat.getType() == RapidCheat.Type.UNION_PAY_CHANNEL) {
                return unionPayService.query(cheat);
            }
            return cheat;
        }

        if (cheat.getStatus() == RapidCheat.Status.CONSUME_FAILED || cheat.getStatus() == RapidCheat.Status.REMIT_FAILED) {
            String exeFlowId = null;
            if (cheat.getRemitFlowId() != null) {
                exeFlowId = cheat.getRemitFlowId();
            } else if (cheat.getConsumeFlowId() != null) {
                exeFlowId = cheat.getConsumeFlowId();
            }

            if (exeFlowId != null) {
                if (cheat.getType() == RapidCheat.Type.UNION_PAY_CHANNEL) {
                    var exeFlow = flowService.getFlowPrecisely(exeFlowId, cheat.getCreateTime());
                    cheat.setFailReason(exeFlow.getFailReason());
                } else if (cheat.getType() == RapidCheat.Type.COMMON_CHANNEL) {
                    var exeFlow = unionPayService.getFlow(exeFlowId, DateUtils.getDateString(cheat.getCreateTime()));
                    cheat.setFailReason(exeFlow.getFailReason());
                }
            }
        }
        throw new ServiceException(ResultCode.ERROR_PARAMETER_WRONG, "信息错误");
    }

    @Override
    public List<RapidCheat> userList(String userId, RapidCheat.Type type, String monthString) {
        var calendar = DateUtils.calendarFromMonthString(monthString);
        var s = calendar.getTime();
        var e = DateUtils.getMonthLastDayLastSecond(s);
        return rapidCheatMapper.userList(userId, type == RapidCheat.Type.ALL ? null : type, s, e);
    }

    private DecimalFormat df = new DecimalFormat("#,##0.00");

    @Override
    public void orderStatusNotify(ExecutionFlow flow, OrderStatus status) {
        orderStatusNotify(flow, status, null);
    }

    @Async
    @Override
    public void asyncOrderStatusNotify(ExecutionFlow flow, OrderStatus status, ExecutionFlow externalFlow) {
        orderStatusNotify(flow, status, externalFlow);
    }

    @Override
    public void orderStatusNotify(ExecutionFlow flow, OrderStatus status, ExecutionFlow externalFlow) {
        String columnName = flow.getType() == ExecutionFlow.Type.PAY ? "consumeFlowId" : "remitFlowId";
        String dateString = DateUtils.getDateString(flow.getCreateTime());
        if (notifyThirdDeveloper(flow, status, dateString)) return; // 异步回调给第三方调用.

        RapidCheat cheat = rapidCheatMapper.queryByExecutionFlow(columnName, flow.getId());
        if (cheat != null) {
            // 快收逻辑
            if (flow.getType() == ExecutionFlow.Type.PAY) {
                if (status == OrderStatus.SUCCEED) {
                    Card remitCard = cardService.cardWithOwnerCheck(cheat.getRemitCardId(), cheat.getUserId());
                    Card consumeCard = cardService.cardWithOwnerCheck(cheat.getConsumeCardId(), cheat.getUserId());
                    String remitId = null;

                    if (externalFlow == null) {
                        remitId = channelDispatcher.remitWithChannelType(
                                flow.getChannelType(),
                                flow.getUserId(),
                                remitCard,
                                consumeCard,
                                cheat.getAmount() // 减去代付费和手续费后才是提现金额
                                        .subtract(cheat.getCommission())
                                        .subtract(cheat.getRemitFee()),
                                cheat.getRemitFee());
                    } else {
                        remitId = externalFlow.getId();
                    }


                    cheat.setRemitFlowId(remitId);
                    cheat.setStatus(RapidCheat.Status.CONSUMED);
                    cheat.setRemitTime(new Date());
                    cheat.setConsumeDoneTime(new Date());

                    userService.setUserActive(flow.getUserId()); // 支付成功 标记为活跃用户

                } else {
                    cheat.setStatus(RapidCheat.Status.CONSUME_FAILED);
                    cheat.setConsumeDoneTime(new Date());
                    messageService.addMessage(Message.builder()
                            .userId(cheat.getUserId())
                            .type(Message.Type.SYSTEM)
                            .title("[快收]交易失败")
                            .content("您刚刚提交的" + df.format(cheat.getAmount()) + "元交易失败, 如有疑问请{{联系客服}}处理")
                            .build(), true);
                }

                rapidCheatMapper.modify(cheat);
            } else if (flow.getType() == ExecutionFlow.Type.REMIT) {
                if (status == OrderStatus.SUCCEED) {
                    cheat.setStatus(RapidCheat.Status.REMITTED);
                    cheat.setRemitDoneTime(new Date());
                    messageService.addMessage(Message.builder()
                            .userId(cheat.getUserId())
                            .type(Message.Type.SYSTEM)
                            .title("[快收]收款成功")
                            .content("您刚刚交易了" + df.format(cheat.getAmount()) + "元, 到账金额为" + df.format(flow.getAmount()) + ", 如有疑问请{{联系客服}}")
                            .build(), true);
                } else {
                    cheat.setStatus(RapidCheat.Status.REMIT_FAILED);
                    cheat.setRemitDoneTime(new Date());
                    messageService.addMessage(Message.builder()
                            .userId(cheat.getUserId())
                            .type(Message.Type.SYSTEM)
                            .title("[快收]收款失败")
                            .content("您刚刚交易了" + df.format(cheat.getAmount()) + "元并支付成功. 应到账金额" + df.format(flow.getAmount()) + "元提现失败, 请{{联系客服}}处理")
                            .build(), true);
                }

                rapidCheatMapper.modify(cheat);
            }
        }

    }

    private boolean notifyThirdDeveloper(ExecutionFlow flow, OrderStatus status, String dateString) {
        try {
            var thirdFlow = thirdPurchaseFlowMapper.flowByExecutionFlowId(dateString, flow.getId());
            if (thirdFlow != null) {
                if (status == OrderStatus.SUCCEED) {
                    userService.setUserActive(flow.getUserId()); // 支付成功 标记为活跃用户
                }

                thirdFlow.setStatus(ThirdPurchaseFlow.Status.CALLBACKING);
                thirdPurchaseFlowMapper.modify(dateString, thirdFlow);

                callbackExecutionFlow(flow, thirdFlow);

                return true;
            }
        } catch (Throwable t) {
            if (t instanceof BadSqlGrammarException) {
                log.warn("catched BadSqlGrammarException, ignored");
            } else {
                log.error(t);
            }
        }
        return false;
    }

    @Override
    public String pay(String channelType, String appId, String userId, String payCardId, BigDecimal orderAmount, String clientIP, String province, String city, String area,
                      String callbackUrl) {
        ChannelDispatcher.checkUserValid(userId);
        var channel = channelDispatcher.getPayChannelInfoByType(channelType);
        var user = userService.getUser(userId);
        var consumeCard = cardService.cardWithOwnerCheck(payCardId, userId);
        var commission = commissionService.calcCommission(channelType, user, cardService.cardWithOwnerCheck(payCardId, userId), orderAmount);
        var payRate = BigDecimal.ONE
                .subtract(BigDecimal.ONE.multiply(commissionService.commissionRate(channelType, user, consumeCard)))
                .multiply(BigDecimal.valueOf(100))
                .setScale(2, RoundingMode.CEILING);
        try {
            var flowId = channel.getPayChannel().tryPay(userId,
                    consumeCard,
                    null,
                    orderAmount,
                    commission,
                    clientIP,
                    payRate.toString(),
                    province,
                    city,
                    area);

            thirdPurchaseFlowMapper.insertFlow(DateUtils.getCurrentDateString(), ThirdPurchaseFlow.builder()
                    .appId(appId)
                    .status(ThirdPurchaseFlow.Status.PENDING)
                    .executionFlowId(flowId)
                    .callbackUrl(callbackUrl)
                    .build());

            return flowId;
        } catch (PayChannelDeadException | TryingBindCardException t) {
            throw new RuntimeException(t);
        }
    }

    @Override
    public String remit(String channelType, String appId, String userId, Card remitCard, Card consumeCard, BigDecimal amount,
                        String callbackUrl) {
        ChannelDispatcher.checkUserValid(userId);
        var channel = channelDispatcher.getPayChannelInfoByType(channelType);
        BigDecimal remitFee = channelFlowService.calculateRemitFee(userService.getUser(userId), amount, ChannelFlowService.RemitType.SCHEDULED);
        try {
            var flowId = channel.getPayChannel().remit(userId, remitCard, consumeCard, amount, remitFee, false, null);
            thirdPurchaseFlowMapper.insertFlow(DateUtils.getCurrentDateString(), ThirdPurchaseFlow.builder()
                    .appId(appId)
                    .status(ThirdPurchaseFlow.Status.PENDING)
                    .executionFlowId(flowId)
                    .callbackUrl(callbackUrl)
                    .build());

            return flowId;
        } catch (RemitCardUnboundException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean checkBind(String channelType, String cardId, String userId, boolean isRemitCard) {
        log.info("check bind of channel: " + channelType);
        var channel = channelDispatcher.getPayChannelInfoByType(channelType);
        var payChannel = channel.getPayChannel();

        var card = cardService.cardWithOwnerCheck(cardId, userId);
        var result =  payChannel.checkIfBound(card, isRemitCard);
        return result;
    }

    @Override
    public String sendBindCardSms(String channelType, String userId, String cardId, String clientIP) {
        var channel = channelDispatcher.getPayChannelInfoByType(channelType);
        return channel.getPayChannel().tryBindCard(userId, cardService.cardWithOwnerCheck(cardId, userId), clientIP);
    }

    @Override
    public String bindCardConfirm(String channelType, String smsCode, String userId, String smsUUID) {
        var channel = channelDispatcher.getPayChannelInfoByType(channelType);
        return channel.getPayChannel().bindCardConfirm(smsCode, userId, smsUUID);
    }

    @Async
    @Override
    public void callbackExecutionFlow(ExecutionFlow flow, ThirdPurchaseFlow thirdPurchaseFlow) {
        var flowToCallback = JSONUtil.objectToMap(ExecutionFlow.builder()
                .id(flow.getId())
                .userId(flow.getUserId())
                .createTime(flow.getCreateTime())
                .status(flow.getStatus())
                .failReason(flow.getFailReason())
                .cardId(flow.getCardId())
                .channelType(flow.getChannelType())
                .type(flow.getType())
                .amount(flow.getAmount())
                .commission(flow.getCommission())
                .remitFee(flow.getRemitFee())
                .build());

        callbackExecutionFlow(flowToCallback, DateUtils.getDateString(flow.getCreateTime()), thirdPurchaseFlow);

    }

    @Async
    @Override
    public void callbackExecutionFlow(com.yhb.yinsheng.model.ExecutionFlow flow, ThirdPurchaseFlow thirdPurchaseFlow) {
        var flowToCallback = JSONUtil.objectToMap(ExecutionFlow.builder()
                .id(flow.getId())
                .userId(flow.getUserId())
                .createTime(flow.getCreateTime())
                .status(flow.getStatus().toCommonChannelStatus())
                .failReason(flow.getFailReason())
                .cardId(flow.getCardId())
                .channelType(ThirdApiController.CHANNEL_TYPE_UNION)
                .type(flow.getType().toCommonChannelType())
                .amount(flow.getAmount())
                .commission(flow.getCommission())
                .remitFee(flow.getRemitFee())
                .build());

        callbackExecutionFlow(flowToCallback, DateUtils.getDateString(flow.getCreateTime()), thirdPurchaseFlow);
    }

    @Override
    public void clearUserReservedBalance(User user) {
        var balances = channelDispatcher.queryUserBalance(user.getId());
        var remitCard = cardService.userDefaultCard(user);
        if (remitCard == null) {
            var cards = cardService.getUserCard(user.getId());
            for (var c : cards) {
                if (c.getType() == Card.Type.DEBIT) {
                    remitCard = c;
                    break;
                }
            }
        }
        if (remitCard != null) {
            Card finalRemitCard = remitCard;
            balances.forEach(c -> {
                try {
                    if (c.getBalance().compareTo(new BigDecimal("20")) >= 0) {
                        channelDispatcher.remitWithChannelType(c.getChannelType(), user.getId(), finalRemitCard, null, c.getBalance().subtract(BigDecimal.ONE), BigDecimal.ONE);
                    }
                } catch (Throwable t) {
                    log.error("ignored err:", t);
                }

            });
        }


    }

    @Override
    public void checkThirdFlow(String dateString, String thirdFlowId) {
        var thirdFlow = thirdPurchaseFlowMapper.flowById(dateString, thirdFlowId);
        var executionFlow = flowService.getFlowPrecisely(thirdFlow.getExecutionFlowId(), dateString);

        callbackExecutionFlow(executionFlow, thirdFlow);
    }

    private void callbackExecutionFlow(Map<String, Object> flowToCallback, String dateString, ThirdPurchaseFlow thirdPurchaseFlow) {

        if (TextUtils.isEmpty(thirdPurchaseFlow.getCallbackUrl())) {
            return;
        }

        flowToCallback.put("timeStamp", System.currentTimeMillis());
        flowToCallback.put("nonceStr", UUID.randomUUID().toString().replace("-", ""));

        String paramsString = SignAndEncryptProcessor.getParamSignString(flowToCallback);

        String sign = calHmac(thirdPurchaseFlow.getAppId(), paramsString);

        if (thirdPurchaseFlow.getCallbackStartTime() == null) {
            thirdPurchaseFlow.setCallbackStartTime(new Date());
        }

        thirdPurchaseFlow.setCallbackRetryTimes(thirdPurchaseFlow.getCallbackRetryTimes() + 1);
        thirdPurchaseFlowMapper.modify(dateString, thirdPurchaseFlow);

        ClientHolder.getClient().newCall(new Request.Builder()
                .header("SIGN", sign)
                .url(thirdPurchaseFlow.getCallbackUrl())
                .post(RequestBody.create(JSONUtil.toJson(flowToCallback), MediaType.parse("application/json;charset=utf-8")))
                .build()).enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                log.error("callback third api err" + thirdPurchaseFlow.getId(), e);
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                var body = response.body();
                if (body != null) {
                    var res = body.string();
                    if ("ok".equals(res)) {
                        thirdPurchaseFlow.setStatus(ThirdPurchaseFlow.Status.CALLBACKED);
                        thirdPurchaseFlowMapper.modify(dateString, thirdPurchaseFlow);
                        log.info("callback third api succ, thirdFlow: " + thirdPurchaseFlow.getId());

                    } else {
                        log.error("callback third api err, resp: " + res + " thirdFlow: " + thirdPurchaseFlow.getId());
                    }

                    return;
                }

                log.error("callback third api err, empty resp body " + thirdPurchaseFlow.getId());

            }
        });
    }

    private String calHmac(String appId, String input) {
        var m = sThirdMacs.get(appId);
        var secret = thirdPartyService.getDeveloper(appId);
        if (m == null) {
            SecretKeySpec spec;
            try {
                m = Mac.getInstance("HmacSHA256");
                spec = new SecretKeySpec(Base64.getDecoder().decode(secret.getSignSecret()), "HmacSHA256");
            } catch (Throwable t) {
                throw new RuntimeException(t);
            }

            sThirdMacs.put(appId, m);
            try {
                m.init(spec);
            } catch (Throwable t) {
                throw new RuntimeException(t);
            }
        }

        return Hex.encodeHexString(m.doFinal(input.getBytes())).toUpperCase();
    }
}
